RegionServer Sizing Rules of Thumb

ars Hofhansl wrote a great blog post about RegionServer memory sizing. 结果是你可能需要比你认为的更多的内存。它受到region size、memstoresize、Hdfs replication factor,以及其他要检查的事项的影响。

Personally I would place the maximum disk space per machine that can be served exclusively with HBase around 6T, unless you have a very read-heavy workload. In that case the Java heap should be 32GB (20G regions, 128M memstores, the rest defaults).

— Lars Hofhansl

http://hadoop-hbase.blogspot.com/2013/01/hbase-region-server-memory-sizing.html

34. 根据列族数量

当前Hbase在处理2或3个以上列族时,表现不是很好,所以控制你schema中的列族数尽量少点。目前flush和compaction是在每个region的基础上进行。所以当一个列族操作大量数据时会引发flush,而相连的列族也会进行flush,即使它们操作的数据量很少。当许多列族存在flush和compaction相互作用时,会产生大量不必要的i/o(这个通过使flush和compaction只针对一个列族来解决)

尽量在你的schema中使用一个列族。只有数据访问固定在特定的列时,可以引入第二个和第三个列族.例如,你有两个列族,但你查询的时候总是访问其中的一个,从来不会两个一起访问。

34.1. 列族的基数

当表中存在多个列在时,注意表的基数(行的数量)。如果ColumnfamliyA有100万行,而ColumnFamilyB有10亿行,CFA的数据可能分散在许多regions中(及RS中),这将导致CFA的scans操作非常低效。

35. 行键设计

35.1. Hotspotting

Hbase中行是按行键的字典序列排序的。这种设计优化scans操作,让你可以将相关的行存储在一起,或靠近的行将一起被读取。当然,row keys设计不好是hotspotting一个常见的原由。hotspotting发生在当大量客户流量流向一个节点或一些节点或一个集群时。这个流量可代表读、写或其他操作。它可能会淹没负责管理这个region的单台机器,导致其性能下降及导致region不可用。也可能会对同一region server管理的其他region产生不好的影响,如不能服务所需的负载。要想集群被充分使用,设计数据访问模式很重要。

避免写操作时的hotspotting,把row key设计成这样:行确实是需要在同一个region。但in the bigger picture,数据会被写倍集群中多个region,而不是一次一个。下面会描述一些常见的避免hotspotting技术,以及它们的优缺点。

Salting

这里的salting与加密无关,指的是在row key的开始中加入随机数据。这里,salting指的是给row key随机配置一个前缀,使它与原本排序不同。可能的前缀数量要与你想要散布数据的region个数相同。如果你有一些“hot”row key模式反复出现在其他更均匀行中,salting会有帮助的。思考下面的例子,Salting可以传播写入到多个负载,以及证明一些读的负面启示。

Example 16. Salting Example

假定你有以下行键表,你的表被分割成一个字母表中的字母一个区域,前缀‘a’是一个区域,‘b’是另一个。这个表中,同一个region中所有的行以f开头。这个例子关注下面这样的行键:

  1. foo0001
  2. foo0002
  3. foo0003
  4. foo0004

现在假定你你将这些分布在四个不同的区域,使用四个不同的salts:a,b,c,d。这个场景中,每个单词前缀都在不同区域上。在使用salts后,你有下面这样的行键。既然你可以写到四个分开的区域,理论上,当写入时,你有四倍的吞吐量比吸入同一region。

  1. a-foo0003
  2. b-foo0001
  3. c-foo0004
  4. d-foo0002

Then, if you add another row, it will randomly be assigned one of the four possible salt values and end up near one of the existing rows.

  1. a-foo0003
  2. b-foo0001
  3. c-foo0003
  4. c-foo0004
  5. d-foo0002

既然这个分配是随机的,如果你想要获得字典序列的行,你要做更多工作。这样,salting增加来写入时的吞吐量,但读时会有消耗。

Hashing

不使用随机分配,你可以使用一个单向散列的方式,可以使已知行总是被同一前缀salted,这种方式是一种在RS上分散负载,而允许读取时可预测的方式。使用确定的hash,允许客户端重建完整的行键以及像平常一样使用Get操作获取完整的行。

Example 17. Hashing Example

Given the same situation in the salting example above, you could instead apply a one-way hash that would cause the row with key foo0003 to always, and predictably, receive the a prefix. Then, to retrieve that row, you would already know the key. You could also optimize things so that certain pairs of keys were always in the same region, for instance.

Reversing the Key

第三个常见的避免hotspotting的技巧是颠倒固定宽度或数量的行键,这样最频繁改变的部分是第一个。这有效地随机化行键,但牺牲了行排序属性。

35.2 单调递增的行键/时间序列数据

在Tom White的Hadoop: The Definitive Guide一书中,有一个章节描述了一个值得注意的问题:在一个集群中,一个导入数据的进程一动不动,所有的client都在等待一个region(就是一个节点),过了一会后,变成了下一个region…如果使用了单调递增或者时序的key就会造成这样的问题。详情可以参见IKai画的漫画monotonically increasing values are bad。使用了顺序的key会将本没有顺序的数据变得有顺序,把负载压在一台机器上。所以要尽量避免时间戳或者(e.g. 1, 2, 3)这样的key。

如果你确实需要上传时间序列数据到Hbase中,可以参考下OpenTSDB,这是个成功的例子。它有一页描述它在Hbase中使用的schema。OpenTSDB中使用了有效的[metric_type][event_timestamp]键格式,这个乍一看会与之前不要在键中使用时间戳的建议相悖。然而,这里的不同之处是没有把时间戳放在开头,这种设计的假设是会有许多不同的metric types,因此即使不断有metric type混合的输入数据流,Puts会被分散到表中各个region上。

35.3. 试着最小化行列尺寸

HBase中,值总是承载着它们的坐标;当cell值在系统中传递时,它总是和它的列、行和时间戳一起。如果你的行和列的名字要是太大(甚至比value的大小还要大)的话,你可能会遇到一些有趣的情况。One such is the case described by Marc Limotte at the tail of HBASE-3551 (recommended!). Therein, the indices that are kept on HBase storefiles (StoreFile (HFile)) to facilitate random access may end up occupying large chunks of the HBase allotted RAM because the cell value coordinates are large. Mark in the above cited comment suggests upping the block size so entries in the store file index happen at a larger interval or modify the table schema so it makes for smaller rows and column names. Compression will also make for larger indices. See the thread a question storefileIndexSize up on the user mailing list.

大部分情况,小的低效没有那么重要。不幸的是,这里却会有影响。不管选择什么模式,列族、行键、属性都会在数据中 重复上亿次。

35.3.1. 列族

列族名要尽可能小。最好用一个字母如用“d”表示data/default

See KeyValue for more information on HBase stores data internally to see why this is important.

35.3.2. 属性

尽管繁琐的属性名(如myVeryImportantAttribute),更好理解,但存在HBase中最好短点(如 via)

See keyvalue for more information on HBase stores data internally to see why this is important.

35.3.3. 行键长度

保持它们合理的短,即它们对被需要的数据获取依然是有用的(e.g., Get vs. Scan)。一个短键对数据访问无用不会比一个具有更好的get/scan性质的长键更好。设计行键需要权衡。

35.3.4. 字节模式

一个long是8字节。可以在这8个字节中存储无符号型数据到18,446,744,073,709,551,615。如果你存储这些数据为字符串,假定一个字节一个字符,你需要大概3倍的字节数。

不信? 下面是示例代码,可以自己运行一下。

  1. //long
  2. //
  3. long l = 1234567890L;
  4. byte[] lb = Bytes.toBytes(l);
  5. System.out.println("long bytes length: " + lb.length); //returns 8
  6. String s = String.valueOf(l);
  7. byte[] sb = Bytes.toBytes(s);
  8. System.out.println("long as string length:" + sb.length); //returns 10
  9. //hash
  10. //
  11. MessageDigest md = MessageDigest.getInstance("MD5");
  12. byte[] digest = md.digets(Bytes.toBytes(s));
  13. System.out.println("md5 digest bytes length:" +digest.length); //returns 16
  14. String sDigest = new String(digest);
  15. byte[] sbDigest = Bytes.toBytes(sDigest);
  16. System.out.println("md5 digest as string length:" +sbdigest.length); //returns 26

不幸的是,使用二进制代表类型,将是你的数据在代码之外很难理解。例如,当你加入一个值时,你将在shell中看到如下情况:

  1. hbasemain):001:0> incr 't','r', 'f:q',1
  2. COUNTER VALUE = 1
  3. hbase(main):002:0> get 't', 'r'
  4. COLUMN CELL
  5. f:q timestamp=1369163040570,
  6. value=\x00\x00\x00\x00\x00\x00\x00\x01
  7. 1 row(s) in 0.0310 seconds

shell尽力去打印字符串,这里它决定仅仅打印16进制。同样的情况将发生在你区域名字内的行键上。如果你了解存储的内容,那就没关系,但是如果任意数据都能被放入到同一cells中,那将变得不可读。这是主要要权衡的地方。

35.4. Reverse Timestamps

Reverse Scan API
HBASE-4811 implements an API to scan a table or a range within a table in reverse, reducing the need to optimize your schema for forward or reverse scanning. This feature is available in HBase 0.98 and later. See https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Scan.html#setReversed%28boolean for more information.

在数据库处理中一个常见问题是快速找到一个值的最近使用的版本。使用反时间戳作为键的一部分对这个问题很有效。Also found in the HBase chapter of Tom White’s book Hadoop: The Definitive Guide (O’Reilly), the technique involves appending (Long.MAX_VALUE - timestamp) to the end of any key, e.g. [key][reverse_timestamp].

表中最近的键值能够通过用【key】进行Scan操作找到并获取第一个记录。因为HBase已经排好序,该键排在任何比它老的行键的前面,所以必然是第一个。

这个技术可以用来替代Number of Versions,其目的是保存“永久”版本,或长时间保存,同时,使用同样的scan方法可以快速访问其他版本。

35.5. 行键和列族

行键在列族范围内。因此,同样的行键可以存在于一个表的每个列族中,而不产生冲突。

35.6. 行键不变性

行键不可以改变。它们唯一被改变的方法是删掉再重新插入。This is a fairly common question on the HBase dist-list so it pays to get the rowkeys right the first time (and/or before you’ve inserted a lot of data).

35.7. 行键和区域分割的关系。

如果你提前分割你的表, 理解你的行键将怎样分布在区域范围上是很重要的。思考下使用可显示的16进制字符在键的开头(e.g.”0000000000000000” to “ffffffffffffffff”)通过Bytes.split获得键的范围,使用了创建区域时的分割策略,10个区域将产生下面的分割

  1. 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 // 0
  2. 54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 // 6
  3. 61 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68 // =
  4. 68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126 // D
  5. 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72 // K
  6. 82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14 // R
  7. 88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44 // X
  8. 95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102 // _
  9. 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 // f

问题是从第二个区域开始所有数据将开始堆叠,最后一个区域将产生一个“lumpy”(或可能是“hot”)区域问题。要理解为什么,参考下ASCII表。 0是字节”48“,f是字节“102”,但是在字节值(58-96)之间有很大的间隔,这可能永不会出现在键值空间中,因为唯一的值是从【0-9】和【a-f】。因此中间区域永远不会被使用。在对这个例子中的键空间进行预分割工作时,需要分割的自定义。

lesson #1: 预分割通常是一个最佳实践,但你需要预分割以所有区域在键空间中都是可访问的方式。这里例子说明了16进制键空间的问题,同样的问题能在任意键空间中发生。了解你的数据。

lesson #2: 通常不建议,使用16进制键依然对预分割表有作用,只要创建的区域在键空间中是可访问的。

下面是一个关于如何进行适当的分割对16进制键:

  1. public static boolean createTable(Admin admin, HTableDescriptor table, byte[][] splits)
  2. throws IOException {
  3. try {
  4. admin.createTable(table, splits);
  5. return true;
  6. } catch (TableExistException e) {
  7. logger.info("table"+table.getNameAsString() + "already exist");
  8. // the table already exists...
  9. return false;
  10. }
  11. }
  12. public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {
  13. byte[][] splits = new byte[numRegions-1][];
  14. BigInteger lowestKey = new BigInteger(startKey, 16);
  15. BigInteger hightestKey = new BigInteger(endKey, 16);
  16. BigInteger range = highestKey.substract(lowestKey);
  17. BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
  18. lowestKey = lowestKey.add(regionIncrement);
  19. for (int i = 0; i < numRegion - 1;i++) {
  20. BigInteger Key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
  21. byte[] b = String.format("%016x", key).getBytes();
  22. splits[i]=b;
  23. }
  24. return splits;
  25. }

36. 版本数

36.1 最大版本数

通过HColumnDescriptor,可以配置每个列族存储的最大版本号。默认最大版本是1。这是一个重要参数原因已在前面数据模型中描述:HBase不会覆盖行值,但是会根据时间保存每行的不同值。多余的版本会在compaction阶段移除。根据应用需要,最大版本数需要增加或减少。

不建议将最大版本数设置到非常大(几百或者更多),除非旧值对你来说非常重要因为这将大大增加StoreFile的大小。

36.2. 版本的最小值

就像行版本的最大值,通过HColumnDescriptors为每个列族配置要保存行版本的最小值。默认最小版本是0,意味着整个功能被禁用了。行版本最小值这个参数和参数time-to-live一起使用,和行版本数结合起来,可实现类似于“keep the last T minutes worth of data, at most N versions, but keep at least M versions around”配置,该参数仅在存活时间对列族启用,且必须小于行版本数。

37. 支持的数据类型

HBase通过Put和Result支持“bytes-in、bytes-out”接口,所以可以被转换成字节数组的anything都可以被存为一个值。输入可以是是字符串、数字、复杂的对象,甚至图像只要它们可以转换为字节。

对值大小的实际限制(如,保存 10-50MB 对象到 HBase 可能对查询来说太长),HBase中所有的行都遵从数据模型,包括版本化,当你设计时将这个也考虑到,以及列族的块大小。

37.1 计数器

一种支持的数据类型,值得一提的是“计数器”(如, 具有原子递增能力的数值)。参考 Table的 Increment 。

同步计数器在区域服务器中完成,不是客户端。

38. 联合

如果有多个表,不要再模式设计中忘了“joins”的潜在因素。

39. Time To Live(TTL)

列族可设置了TTL秒数,一旦达到过期时间,HBase会自动删除行。这个适用于行的所有版本,即使当前版本。HBase里面TTL 时间时区是 UTC。

仅包含过期行的是store file在minor compaction时被删除。设置hbase.store.delete.expired.storefile为false,可使这个功能失效。将最小版本号设置为非零也可使这个功能失效。

See HColumnDescriptor for more information.

近来Hbase的版本也支持在cell基础上设置TTL。See HBASE-10560 for more information.Cell TTLs被提交作为变化请求的属性(如Appends,Increments,Puts等),使用 Mutation#设置TTL。如果设置了TTL属性,这将被用在所有的cells上,这些cells是服务器上被操作刚更新过的那些。对于cell TTL操作和列族TTLs,这里有两个值得注意的区别:

  • cell TTLs以毫秒表示而不是秒
  • cell TTLs不能延长cell的生命周期至超过列族的的设置

40. 保留删除的cells

默认情况下,删除标记可追溯至时间的开始(对所有时间状态有效)。因此,Get或Scan操作将不会得到一个删除的cell(row或column),即使Get/Scan操作想要获取删除标记之前一段时间范围的cell。

列族可以可选的(optionally)保存删除的cells。这样,删除的cells依然可以被恢复,只要这些操作指定特定的时间范围,这个时间范围在任何删除操作影响cells的时间戳之前结束,这允许时间上的查询,即使存在删除。

被删除的cells依然遵从TTL并且永远不会多于“最大版本数”的被删除cells。新的“raw”scan选项可以列出所有被删除的行和删除者。

Example 18. 使用HBase Shell来改变 KEEP_DELETED_CELLS

  1. hbase> hbase> alter t1′, NAME => f1′, KEEP_DELETED_CELLS => true

Example 19. 使用API改变KEEP_DELETED_CELLS

  1. ...
  2. HColumnDescriptor.setKeepDeletedCells(true);
  3. ...

下面展示下改变KEEP_DELETED_CELLS值会对表产生的基本影响。

首先,没有: create ‘test’, {NAME=>’e’, VERSIONS=>2147483647} put ‘test’, ‘r1’, ‘e:c1’, ‘value’, 10 put ‘test’, ‘r1’, ‘e:c1’, ‘value’, 12 put ‘test’, ‘r1’, ‘e:c1’, ‘value’, 14 delete ‘test’, ‘r1’, ‘e:c1’, 11

  1. hbase(main):017:0> scan 'test', {RAW=>true, VERSION=>1000}
  2. ROW COLUMN+CELL
  3. r1 column=e:c1, timestamp=14, value= value
  4. r1 column=e:c1, timestamp=12, value= value
  5. r1 column=e:c1, timestamp=11, type=DeleteColumn
  6. r1 column=e:c1, timestamp=10, value=value
  7. 1 row(s) in 0.0120 seconds
  8. hbase(main):018:0> flush 'test'
  9. 0 row(s) in 0.0350 seconds
  10. hbase(main):0.19:0> scan 'test', {RAW=>true, VERSION=>1000}
  11. ROW COLUMN+CELL
  12. r1 column=e:c1, timestamp=14, value=value
  13. r1 column=e:c1, timestamp=12, value=value
  14. r1 column=e:c1, timestamp=11, type=DeleteColumn
  15. 1 row(s) in 0.0120 seconds
  16. hbase(main):020:0> major_compact 'test'
  17. 0 row(s) in 0.0260 seconds
  18. hbase(main):021:0> scan 'test', {RAW=>true, VERSIONS=>1000}
  19. ROW COLUMN+CELL
  20. r1 column=e:c1, timestamp=14, value=value
  21. r1 column=e:c1, timestamp=12, value=value
  22. 1 row(s) in 0.0120 seconds

注意删除cells是怎样生效的

现在进行同一个测试,该表设置了KEEP_DELETED_CELLS(可以以表为单位或以列族为单位进行):

  1. hbase(main):005:0> create 'test', {NAME=>'e', VERSIONS=>2147483647, KEEP_DELETED_CELLS => true}
  2. 0 row(s) in 0.2160 seconds
  3. => Hbase::Table - test
  4. hbase(main):006:0> put 'test', 'r1', 'e:c1', 'value', 10
  5. 0 row(s) in 0.1070 seconds
  6. hbase(main):007:0> put 'test', 'r1', 'e:c1', 'value', 12
  7. 0 row(s) in 0.0140 seconds
  8. hbase(main):008:0> put 'test', 'r1', 'e:c1', 'value', 14
  9. 0 row(s) in 0.0160 seconds
  10. hbase(main):009:0> delete 'test', 'r1', 'e:c1', 11
  11. 0 row(s) in 0.0290 seconds
  12. hbase(main):010:0> scan 'test', {RAW=>true, VERSIONS=>1000}
  13. ROW COLUMN+CELL
  14. r1 column=e:c1, timestamp=14, value=value
  15. r1 column=e:c1, timestamp=12, value=value
  16. r1 column=e:c1, timestamp=11, type=DeleteColumn
  17. r1 column=e:c1, timestamp=10, value=value
  18. 1 row(s) in 0.0550 seconds
  19. hbase(main):011:0> flush 'test'
  20. 0 row(s) in 0.2780 seconds
  21. hbase(main):012:0> scan 'test', {RAW=>true, VERSIONS=>1000}
  22. ROW COLUMN+CELL
  23. r1 column=e:c1, timestamp=14, value=value
  24. r1 column=e:c1, timestamp=12, value=value
  25. r1 column=e:c1, timestamp=11, type=DeleteColumn
  26. r1 column=e:c1, timestamp=10, value=value
  27. 1 row(s) in 0.0620 seconds
  28. hbase(main):014:0> scan 'test', {RAW=>true, VERSIONS=>1000}
  29. ROW COLUMN+CELL
  30. r1 column=e:c1, timestamp=14, value=value
  31. r1 column=e:c1, timestamp=12, value=value
  32. r1 column=e:c1, timestamp=11, type=DeleteColumn
  33. r1 column=e:c1, timestamp=10, value=value
  34. 1 row(s) in 0.0650 seconds

KEEP_DELETED_CELLS是为了避免删除操作仅仅是删除操作者。所以打开KEEP_DELETED_CELLS被删除的cells被真正移除,是在我们写入比配置更多的版本,或者我们设置了TTL并且CELLS超过配置的超时时间。

41. Secondary Indexes and Alternate Query Paths

这小节的标题也可以是“如果我的rowkey是这样的而我想那样查询怎么办”,一个常见的分布表例子:rowkey的形式为”user-timestamp”,但有跨过user使用特定时间范围的活动需要,因此,依据user来选择比较简单,因其正在开头位置,但时间不是。

对于这个问题没有最好的解决方法,因为它依赖于:

  • 使用者数量
  • 数据大小和数据到达速率
  • 报告要求的灵活性(如, 完全点对点的日期选择 vs. 预先配置的范围)
  • 期望执行查询的速度(如,对于一些点对点的报告90秒或许是合理的,但对其他情况这可能太长了)

方法也会受集群大小以及你在这个方法上投入多少精力的影响,下面是常见的技术,这是一些综合但不过详尽的方法表

不应该对第二索引需要额外的集群空间和处理感到奇怪。在RDBMS中也存在这样的情况,因为创建另外一个索引需要空间和处理周期来更新。RDBMS产品在处理替换索引的箱外管理上更高级,而hbase具有更大的数据量,这是一个功能的权衡。

当使用这些方法的时候,注意Apache HBase Performance Tuning。

41.1. 过滤查询 Filter Query

依据一些情况,使用Client Request Filters可能更合适。这种情况下,不会创建第二索引。然而,在这样情况下(如单线程客户端),不要在一个大表上尝试全扫描。

41.2. 周期性更新的第二索引 Periodic-Update Secondary Index

第二索引可以在另外一张表中建立起来,这张表会通过MR job进行更新。job可以在日内被执行,但是依据加载策略,它存在与主要数据表不同步的可能性。

41.3 双写第二索引Dual-Write Secondary Index

另一个策略是在像集群出版数据(publishing data)时,建立第二索引(如向表中写数据,就向索引表中写数据)。 如果这个方法发生在某个数据表已经存在,对第二索引来说,需要MR job的引导。

41.4 总结表 Summary Tables

时间范围宽的地方,数据量也很大,总结表是一种常见的方法。这会随着MR job产生到别的表中。

41.5 第二索引协处理器Coprocessor Secondary Index

协处理器就像RDBMS中的triggers。详见coprocessor小节。

42. 约束 Constraints

当前,HBase支持传统数据库的‘Constraints’用法。明智的约束用法是对表的属性执行相关业务规则(如 确保值在1-10范围内)。约束也用在实施参照完整性上,但这被强烈的反对,因为当完成性检查打开时,它将大幅降低表格的写入能力。更多的约束使用方法可以查阅0.94版之后的Constraint小节

43. 表格设计案例学习 Schema Design Case Studies

下面将介绍一些典型的HBase数据提取用例, 并介绍如何行键设计和建立如何近似。 注意:这只是对潜在方法的举例,无法穷尽。了解你的数据,了解你的处理需求。

强烈建议你在阅读下面案例学习之前,先阅读HBase and Schema Design剩余部分。

下面案例主要研究:

-日志数据/时间序列数据 Log Data / Timeseries Data

-日志数据/时间序列的类固醇 Log Data/Timeseries on Steroids

-顾客/预订 Customer/Order

-Tall/Wide/Middle Schema Design

-列表数据 List Data

43.1.案例学习-日志数据和时间序列数据

假定我们正在收集下面的数据元素。

  • Hostname
  • Timestamp
  • Log event
  • Value/message

我们可以将这些存在HBase表的LOG_DATA中,但是这些的rowkey是什么呢?rowkey将是来自hostname、timestamp和log-event这些属性的某些组合,具体是什么呢?

43.1.1 时间戳在Rowkey的开头位置

行键[timestamp][hostname][log-event]受到单调递增的行键问题,这个问题在35.2小节Monotonically Increasing Row Keys/Timeseries Data中有描述。

有另外一种关于“bucketing”时间戳的模式时常在分布式表中被提及,通过对时间戳进行取模运算。如果以时间导向的扫描是重要的,这可能是有效的方法。一定要注意通道号,因为需要和扫描操作同样的通道号码来返回结果。

  1. long bucket = timestamp % numBuckets

构建:

  1. [bucket][timestamp][hostname][log-event]

就像上面提及的,要选择特定时间范围的数据,需要对每个通道进行扫描操作。例如,100个通道将在keyspace中提供一个广泛的分布,但这将需要100次扫描来获取每个时间戳的数据,这是一种权衡。

43.1.2 Host在Rowkey的开头位置

如果有大量主机从键空间读写,行键[hostname][log-event][timestamp]是一个候选。如果以主机名为首要扫描条件,这个方法会起作用。

43.1.3. 时间戳还是反时间戳

如果最重要的访问路径是对最近的事件,那么反过来储存时间戳(如,timestamp = Long.MAX_VALUE – timestamp)将创出能够通过扫描[hostname][log-event]快速获取最近发生事件的性质。

这个方法是否正确,要看什么最适合当前形势。

43.1.1.Rowkeys长度可变还是固定

在HBase中记住行键是对应每一列的是很重要的。如果hostname是a,而时间类型是e1,那么结果行键将会很小。然而,如果摄入的主机名是myserver1.mycompany.com时间类型是com.package1.subpackage2.subsubpackage3.ImportantService情况又会是怎样呢?

或许在行键中使用一些替代应该会有用。这里至少有两种方法:散列和数字。以主机名为行键的主要位置为例,可能是这样的:

将行键与散列结合起来:

  • [MD5 hash of hostname] = 16 bytes
  • [MD5 hash of event-type] = 16 bytes
  • [timestamp] = 8 bytes

将行键用数字替换:

对这种方法,除了LOG_DATA额外的查询表也是需要的,叫做LOG_TYPES。LOG_TYPES的行键可能是:

  • [type] eg.,字节表示hostname vs.event-type
  • [bytes] 原始hostname或event-type的可变长度字节

这个行键的一列可能是一个指定数的长整型,可以通过使用HBase counter获得。

所以最终组合的行键会是:

  • [substituted long for hostname] = 8字节
  • [substituted long for event type] = 8字节
  • [timestamp] = 8字节

在使用散列或数值的这两种方法中, hostname或event-type的原始数据都可以在列中被储存。

43.2 Case Study - Log Data and Timeseries Data on Steroids

这实际上是OpenTSDB方法。OpenTSDB是每个特定的时间重写数据包行到列中。For a detailed explanation, see: http://opentsdb.net/schema.html, and Lessons Learned from OpenTSDB from HBaseCon2012.

但这里是一般概念如何工作的:数据是被摄入的,例如,以这种方式

  1. [hostname][log-event][timestamp1]
  2. [hostname][log-event][timestamp2]
  3. [hostname][log-event][timestamp3]

对每个详细事件,使用分开的行键,重写成这样:

  1. [hostname][log-event][timerange]

并且每个上面的事件都被转换到列中,和相对于开始时间范围的时间偏移一起存储(如,每隔5分钟)这是一种相当高级的处理技巧,但HBase实现了它。

43.3 案例学习-顾客/订单 Customer/Order

假定Hbase用来储存客户和订单信息,这里有两种主要的记录类型被摄入:一个顾客记录类型和订单记录类型

顾客记录类型将包含所有你通常想要的事情:

  • 顾客号码
  • 顾客名字
  • 地址
  • 电话

订单记录类型将会包含:

  • 顾客号码
  • 订单号
  • 销售日期
  • 一系列关于配送位置和产品线产品的关联对象信息

假定顾客号和销售订单组合起来唯一确定一个订单,这两个属性将组成行健,具体是一个符合键如:

  1. [customer number][order number]

对于一个ORDER表。当然,这里还有许多设计决定要做:原始数值对于行健是最好的选择么?

我们也会遇到同样的设计问题在日志数据用例中。顾客号的键空间是什么?样式是什么样的?(如,文字数字式的还是数字式的?)因为在HBase中使用固定长度键是有优势的,这样键可以支持在键空间中的合理扩展,相似的选项会出现:

散列复合Rowkey:

  • [MD5 of customer number] = 16bytes
  • [MD5 of order number] = 16bytes

Composite Numeric/Hash Combo Rowkey 复合数字/散列组合rowkey:

  • [substituted long for customer number] = 8bytes
  • [MD5 of order number] = 16bytes

43.3.1. 单表?多表?

传统的设计方法将把表分成CUSTOMER和SALES.另一种选择是将多个纪录类型打包到一个表中(如 CUSTOMER++)。

Customer Record Type Rowkey:

  • [customer-id]
  • [type] = 类型表示‘1’为顾客纪录类型

Order Record Type Rowkey:

  • [customer-id]
  • [type] = 类型表示‘2’为订单纪录类型
  • [order]

这里特殊的CUSTOMER++方法的优点是通过customer-id组织许多不同的纪录类型(如 一次扫描操作可以得到关于那个客户的所有信息)。而缺点是扫描获取特定纪录类型不太容易。

43.3.2.订单对象设计

现在我们要解决如何给订单对象建模,假定阶级结构是下面这样的:

Order (一个订单会有多个投递地址)

LineItem(一个投递地址会有多个项目)

有多个选项关于存储这些数据

完全标准化

这个方法中分别有表:ORDER,SHIPPING_LOCATION 和 LINE_ITEM. The ORDER table’s rowkey was described above: 43. schema.casestudies.custorder

SHIPPING_LOCATION的合成行键应该是这样的:

  • [order-rowkey]
  • [shipping location number] e.g., 1st location, 2nd, etc.
  • [line item number] e.g., 1st lineitem, 2nd, etc.

这种归一化方法与RDBMS中的方法很像,但是那不是你在HBase中的唯一选择。这种方法的缺点是要想获得任意Order的信息,你需要:

  • 获取订单的订单表
  • 扫描订单的SHIPPING_LOCATION表来获取ShippingLocation instances
  • 扫描LINE_ITEM来获取每个ShippingLocation

这是RDBMS会在底下至少会完成的,但由于没有加入到HBase中,你只是更加意识到这个事实。(this is what an RDBMS would do under the covers anyway, but since there are no joins in HBase you’re just more aware of this fact.)

具有纪录类型的单表

这种方法,只存在一个表ORDER,它包含行健在上面小节描述的:

  • [order-rowkey]
  • [ORDER record type]

ShippingLocation 组成行健:

  • [order-rowkey]
  • [SHIPPING record type]
  • [shipping location number]

The LineItem composite rowkey would be something like this:

  • [order-rowkey]
  • [shipping location number] e.g., 1st location, 2nd, etc.
  • [line item number] e.g., 1st lineitem, 2nd, etc.

非标准化

具有纪录类型的单表的一个变化方法是非标准化和扁平化对象层次,例如去掉ShippingLocation属性在每个LineItem实例上

The LineItem composite rowkey would be something like this:

  • [order-rowkey]
  • [LINE record type]
  • [line item number] e.g., 1st lineitem, 2nd, etc., care must be taken that there are unique across the entire order

and the LineItem columns would be something like this:

  • itemNumber
  • quantity
  • price
  • shipToLine1 (denormalized from ShippingLocation)
  • shipToLine2 (denormalized from ShippingLocation)
  • shipToCity (denormalized from ShippingLocation)
  • shipToState (denormalized from ShippingLocation)
  • shipToZip (denormalized from ShippingLocation)

这种方法的优点包括一个不太复杂的对象层次结构,但是一个缺点是更新会变得更复杂以防任意信息改变。

对象BOLB

对于这种方法,整个订单的对象图是以某种方式按照BLOB处理,例如,the ORDER table’s rowkey was described above: schema.casestudies.custorder,单独称作“order”的一列将包含一个被反序列化的对象,它包含一个订单、ShippingLocations和LineItems的container

这里有许多可以选择:JSON, XML, Java Serialization, Avro, Hadoop Writables, etc.这些都是同一中方法的变体:将对象图编码到一个字节数组。注意这种方法要确保向后兼容性,以防对象模型变化,这样旧版留下来的结构依然可以回读HBase。

优点是可以用小的I/O实现对复杂对象图的管理(如,这个例子中 a single HBase Get per Order)。缺点是上述警告的向后兼容性、序列化语言的依赖性(e.g.,java序列化只能在java client上运行 ),事实上,你需要反序列化整个对象来获得BLOB内部一些信息块,以及使HIVE这样框架与自定义对象进行工作的难度。

43.4 Case Study - “Tall/Wide/Middle” Schema Design Smackdown

这小节将描述额外的在分布式表格中出现的表格设计问题,尤其是tall and wide tables。这只是一般的指导方针,而不是定律,每个应用都要考虑其本身需求。

43.4.1 Rows vs. Versions

应该选择rows还是HBase’s内建版本是一个常见的问题。这一情形通常是保存许多行的版本的地方(如,where it is significantly above the HBase default of 1 max versions)。行方法将要求在行键的某些部分处存储时间戳,这样连续更新时不会被覆盖。

一般来说,更倾向行。

43.4.2 Rows vs. Columns

Another common question is whether one should prefer rows or columns.这种情况一般会发生在极端的宽表中,如1行有100万属性或1百万行每行只有一列。

通常选择行。要清楚,整个指导原则是在极端宽的情形下,而不是在存储几十个或几百个列的标准用例中。But there is also a middle path between these two options, and that is “Rows as Columns.”

43.4.3 Rows as Columns

Rows vs. Columns的中间方法是对某些行,将本是不同行的数据打包成列。OpenTSDB是这个方法最好的例子,单行代表定义好的时间范围,然后具体的事例被当成列。这种方法通常更加复杂,或许要求重写数据的额外复杂性,但也有I/O高效的优点。

43.5 Case Study - List Data

The following is an exchange from the user dist-list regarding a fairly common question: 在apache HBase中如何处理每个用户

  • QUESTION*

我们正设法找到如何在HBase中存储大量表格数据,并试着确定那种连接模式最有意义。其中一个选择是将数据的主要内容存储于key中,如此我们得到:

  1. <FixedWidthUserName><FixedWidthValueId1>:"" (no value)
  2. <FixedWidthUserName><FixedWidthValueId2>:"" (no value)
  3. <FixedWidthUserName><FixedWidthValueId3>:"" (no value)

另一种方法是我们实现这个完全使用:

  1. <FixedWidthUserName><FixedWidthPageNum0>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...
  2. <FixedWidthUserName><FixedWidthPageNum1>:<FixedWidthLength><FixedIdNextPageNum><ValueId1><ValueId2><ValueId3>...

这里每行都要包含多个值。所以获取第一个三十个值应该是这样的:

  1. scan { STARTROW => 'FixedWidthUsername' LIMIT => 30}

第二次:

  1. get 'FixedWidthUserName\x00\x00\x00\x00'

一般的使用方法是只读表格最开始的30个值,因为不经常更深一步访问表格。一些使用者在表中只有30个值,而有的则有几百万。

单值模式看起来将占用Hbase中更多的空间,但提供某些改进的检索、分页的灵活性。会不会有某些有显著的性能优势对于能够paginate via gets 和 paginating with scans?

我最初的理解是在对页面大小未知的情况下scan操作将会更快!(适当的设置cashing),但是如果我们总是请求同样大小的页面,get操作应该会更快。 I’ve ended up hearing different people tell me opposite things about performance. I assume the page sizes would be relatively consistent, so for most use cases we could guarantee that we only wanted one page of data in the fixed-page-length case. I would also assume that we would have infrequent updates, but may have inserts into the middle of these lists (meaning we’d need to update all subsequent rows).

  • ANSWER *

如果我理解没有理解错,你最终要在表中存储三个值“user,valued, value”,对吧?如:

  1. "user123, firstname, Paul",
  2. "user234, lastname, Smith"

(username和valueids也是固定宽度)

存取模式类似如下形式:“对user X, 从valueid Y开始列出之后30个值”对吧?这些值应该按valueid返回分类?

tl:dr版本是你应该对每个user+value行使用的样式,而不是建立一个复杂的行内分页表除非你确定你真的需要它。

你的两个选择反映了人们设计HBase表时常遇到的问题:我该选择“tall”还是“wide”?第一个表是“tall”,每行代表一个用户的一个值,并且每个用户在表中有许多行;行键是user+valueid, 这里只会有一个列族代表“the value”。 This is great if you want to scan over rows in sorted order by row key (thus my question above, about whether these ids are sorted correctly). You can start a scan at any user+valueid, read the next 30, and be done. What you’re giving up is the ability to have transactional guarantees around all the rows for one user, but it doesn’t sound like you need that. Doing it this way is generally recommended (see here http://hbase.apache.org/book.html#schema.smackdown).

第二个选择是“wide”:在一行中存储一堆数据,使用不同的qualifiers(在这里qualifier代表valueid)。简单的做法是对于一个user将所有values存储在单行中。我猜你跳入分页版本中,因为你假定百万列存储在单行中会影响性能,这或许并不为真。只要你不试着在一个请求中做太多,或扫描以返回行中的所有cells这样类似的操作,它基本上不会更坏。客户端有方法能够让你获得列的特定分片。

注意没有方法基本上使用更多硬盘空间比另一种,你仅仅是转移某个值的识别信息到行键或到column qualifier中,在内部,每个键/值依然存储整个行键和列族名。

手动分页版本更加复杂,像你提及那样,要记录每页中的许多事情,当有新值插入时要re-shuffling等。这看起来更加复杂,它或许有点速度优势(或缺点)在极端高吞吐时,唯一知道它会怎样的方法就是try it out.

44.Operational and Performance Configuration Options 操作和性能配置选项

44.1. Tune HBase Server RPC Handling

  • Set hbase.regionserver.handler.count (in hbase-site.xml) to cores x spindles for concurrency.用于x核心并发。
  • Optionally, split the call queues into separate read and write queues for differentiated service. 可选地,将呼叫队列拆分为针对不同服务的读写队列。The parameter hbase.ipc.server.callqueue.handler.factor specifies the number of call queues:

    0 代表一条单一的共享队列
    1 代表每条队列对应一个处理程序
    在0 和1 之间的值,根据处理程序的数量按比例分配队列的数量。例如,0.5代表两个处理程序之间共享一个队列。

  • Use hbase.ipc.server.callqueue.read.ratio (hbase.ipc.server.callqueue.read.share in 0.98) to split the call queues into read and write queues:将呼叫队列分为读写队列

0.5 代表读写队列数量相等

<0.5 代表read多
> 0.5 代表write多

  • Set hbase.ipc.server.callqueue.scan.ratio (HBase 1.0+) to split read call queues into small-read and long-read queues:

0.5 means that there will be the same number of short-read and long-read queues
< 0.5 for more short-read
> 0.5 for more long-read

44.2. 禁用Nagle RPC

禁用Nagle算法。 延迟的ACKs可以总计达~ 200ms RPC往返时间。设置下面这些参数:

  • In Hadoop’s core-site.xml:

    ipc.server.tcpnodelay = true ipc.client.tcpnodelay = true

  • In HBase’s hbase-site.xml:

    hbase.ipc.client.tcpnodelay = true hbase.ipc.server.tcpnodelay = true

43.3. 限制服务器失败的影响

尽快检测到regionserver错误。设置下面的参数:

  • In hbase-site.xml, set zookeeper.session.timeout to 30 seconds or less to bound failure detection (20-30 seconds is a good start).
  • Detect and avoid unhealthy or failed HDFS DataNodes: in hdfs-site.xml and hbase-site.xml, set the following parameters:

    dfs.namenode.avoid.read.stale.datanode = true dfs.namenode.avoid.write.stale.datanode = true

44.4. Low Latency服务器端的优化

  • 跳过本地块网络。In hbase-site.xml, set the following parameters:

    dfs.client.read.shortcircuit = true dfs.client.read.shortcircuit.buffer.size = 131072 (Important to avoid OOME)

  • 确保数据本地化。In hbase-site.xml, set hbase.hstore.min.locality.to.skip.major.compact = 0.7 (Meaning that 0.7 <= n <= 1)

  • 确保DN有足够的用于块传输的处理器。 In hdfs-site.xml, set the following parameters:

    dfs.datanode.max.xcievers >= 8192
    dfs.datanode.handler.count = number of spindles

44.5 JVM 调优

44.5.1 Tune JVM GC for low collection latencies

  • Use the CMS collector: -XX:+UseConcMarkSweepGC
  • Keep eden space as small as possible to minimize average collection time. Example:

    -XX:CMSInitiatingOccupancyFraction=70

  • Optimize for low collection latency rather than throughput: -Xmn512m
  • Collect eden in parallel: -XX:+UseParNewGC
  • Avoid collection under pressure: -XX:+UseCMSInitiatingOccupancyOnly
  • Limit per request scanner result sizing so everything fits into survivor space but doesn’t tenure. In hbase-site.xml, set hbase.client.scanner.max.result.size to 1/8th of eden space (with -Xmn512m this is ~51MB )
  • Set max.result.size x handler.count less than survivor space

45. Special Cases

45.1 对应用来说快速失败比等待要好

  • In hbase-site.xml on the client side, set the following parameters:

    Set hbase.client.pause = 1000
    Set hbase.client.retries.number = 3
    If you want to ride over splits and region moves, increase hbase.client.retries.number substantially (>= 20)
    Set the RecoverableZookeeper retry count: zookeeper.recovery.retry = 1 (no retry)

  • In hbase-site.xml on the server side, set the Zookeeper session timeout for detecting server failures: zookeeper.session.timeout ⇐ 30 seconds (20-30 is good).

45.2 For applications that can tolerate slightly out of date information